#!/usr/bin/python -S
"""Language-independent tests for JSON Pattern.

Uses the testy test framework.
"""

__author__ = 'Andy Chu'


import os
import sys

from pan.core import cmdapp
from pan.core import params
from pan.core import json
from pan.core import util
from pan.test import testy

if __name__ == '__main__':
  # for jsonpattern package
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'python'))
  # Root of the project
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

from tests import test_main

import jsonpattern  # module under *direct* test

# HACK
from jsonpattern import _exceptions

B = util.BlockStr


class AtomTests(testy.Test):

  LABELS = ['multilanguage']

  def testTrivial(self):
    # Empty string matches anything
    self.verify.Match('P|', '', {})
    self.verify.Match('P|', ' ', {})
    self.verify.Match('P|', 'a', {})

    self.verify.Match('P| a', 'a', {})
    self.verify.NoMatch('P| a', 'b')

  def testIntegerFilter(self):
    self.verify.Match(
        r'P| [a-z]+ { \d+ size | int }',
        'a43',
        {'size': 43})

    self.verify.NoMatch(
        r'P| [a-z]+ { \d+ size | int }', '43')

  def testSpace(self):
    # TODO: Not sure if {.space} is the best syntax
    self.verify.Match('P| {.space}', ' ', {})
    self.verify.NoMatch('P| {.space}', '')

  def testLiterals(self):
    self.verify.Match('P| {.newline}', '\n', {})
    self.verify.NoMatch('P| {.newline}', '\t')

    self.verify.Match('P| {.tab}', '\t', {})
    self.verify.Match('P| {.lbrace}', '{', {})

    self.verify.ParseTree('P| a{.space}b')
    self.verify.Match('P| a{.space}b', 'a b', {})
    self.verify.NoMatch('P| a{.space}b', 'a.b')

  def testLiteralsCombineWithRegexes(self):
    self.verify.Match('P| {.newline}+', '\n\n', {})
    self.verify.NoMatch('P| {.newline}+', '')

  def testCharacterGroupLiterals(self):
    self.verify.Match('P| {.word}+', 'abc', {})
    self.verify.NoMatch('P| {.word}+', '%%')

    self.verify.Match('P| {!word}+', '%%', {})
    self.verify.NoMatch('P| {!word}+', 'abc')

    self.verify.Match('P| {.digit}+', '012', {})
    self.verify.NoMatch('P| {.digit}+', 'abc')

    self.verify.Match('P| {!digit}+', 'abc', {})
    self.verify.NoMatch('P| {!digit}+', '012')

  def testCharacterClassLiterals(self):
    p = 'P| {.chars [ ] ^ - .word}+'
    self.verify.Match(p, '[]^-abc[]', {})
    self.verify.NoMatch(p, '%%[]^-abc[]')

  def testRegexRepetitionRangesAreNotDirectives(self):

    # Special case for {} repetition syntax
    self.verify.Match('P| a{3,6}', 'aaa', {})

    self.verify.Match('P| a{3,6}$', 'aaa', {})
    self.verify.Match('P| a{3,6}$', 'aaaa', {})
    self.verify.Match('P| a{3,6}$', 'aaaaa', {})
    self.verify.Match('P| a{3,6}$', 'aaaaaa', {})
    self.verify.NoMatch('P| a{3,6}$', 'aaaaaaa')  # 7 is too many
      
  def testEscapedBackslash(self):

    # Special case for {} repetition syntax
    p = r'P| \w{.space}$'
    self.verify.Match(p, r'a ', {})

    # Two backslashes in a regex matches a literal backslash string
    p = r'P| \\{.space}$'
    self.verify.Match(p, r'\ ', {})
    self.verify.NoMatch(p, r'a ')

  def testEscapedMetacharacter(self):

    # Special case for {} repetition syntax
    p = r'P| \{\}'
    self.verify.Match(p, r'{}', {})
    self.verify.NoMatch(p, r'..')

    # Make sure that we don't split this into 2 regexes
    p = r'P| (\{\})'
    self.verify.Match(p, r'{}', {})
    self.verify.NoMatch(p, r'..')

  def testLiteralMetacharacters(self):
    # Special case for {} repetition syntax
    self.verify.Match(r'P| \{3,6\}', '{3,6}', {})
    self.verify.NoMatch(r'P| \{3,6\}', '{2,6}')
    self.verify.Match(
        """
         | $value foo
        P|   \{3,6\}
         | end
        """, '{3,6}', {'foo': '{3,6}'})

  def testNamedValueShorthand(self):
    p = "P| a{ \d+ digits }b"
    self.verify.ParseTree(p)
    s = "a34b"

    d = {'digits': '34'}
    self.verify.Match(p, s, d)

  def testNamedValueShorthandWithFilter(self):
    p = "P| a{ \d+ digits|int }b"
    s = "a34b"

    d = {'digits': 34}
    self.verify.Match(p, s, d)

  def testNamedValueShorthandWithAlternateDelimiters(self):
    # Since the pattern contains an internal space, we need to use different
    # delimiters.
    p = "P| a{/[ ]/ a-space }b"
    s = "a b"

    d = {'a-space': ' '}
    self.verify.Match(p, s, d)


class BlockTests(testy.Test):

  LABELS = ['multilanguage']

  def testSimpleRepeated(self):
    p = """
        P| R=
         | repeated * reviewers
        P|     { \w+ } ,?
         | end
        """
    self.verify.Match(p, 'R=foo', {'reviewers': ['foo']})
    self.verify.Match(p, 'R=foo,bar', {'reviewers': ['foo', 'bar']})

  def testRepeatedRecord(self):
    p = """
        | repeated files
       P|     { [a-z]+ word } { \d+ number }
        | end
        """
    self.verify.Match(p, 'ab34bc45',
        { 'files': [
          { 'word': 'ab', 'number': '34' },
          { 'word': 'bc', 'number': '45' },
          ]})

  def testBadRepeated(self):
    # ** is not legal repetition
    p = """
         | repeated ** files
        P|   { [a-z]+ word } { \d+ number }
         | end
        """
    self.verify.CompilationError(_exceptions.PatternSyntaxError, p)

  def testAnonymousRepeated(self):
    p = """
         | $repeated
        P|     { [a-z]+ word } { \d+ number }
         | end
        """
    self.verify.Match(p, "ab3bc4",
        [ {'word': 'ab', 'number': '3'},
          {'word': 'bc', 'number': '4'}])

  def testRepeatedNamed(self):
    p = """\
         | repeated * files
        P|     ...[ ]{ [^#]+ filename }#{ \d+ revision }[ ]{ \w+ action }\s*
         | end
        """
    s = """\
... foo.py#13 add"""

    d = {'files': [{'action': 'add', 'revision': '13', 'filename': 'foo.py'}]}
    self.verify.Match(p, s, d)

    s = """\
... foo.py#13 add
... baz/bar.cc#5 edit"""

    d = {'files': [
          {'action': 'add', 'revision': '13', 'filename': 'foo.py'},
          {'action': 'edit', 'revision' : '5', 'filename': 'baz/bar.cc'}]
        }
    self.verify.Match(p, s, d)

  def testNamedThenRepeated(self):
    # TODO: Make it so the newline at the end doesn't screw you up!
    p = """\
        P| Change[ ]{ \d+ change-num }\s*
         | repeated * files
        P|     ...[ ]{ [^#]+ filename }#{ \d+ revision }[ ]{ \w+ action }\s*
         | end
        """
    s = """\
Change 35
... foo.py#13 add
"""

    d = {
        'change-num': '35',
        'files': [{'action': 'add', 'revision': '13', 'filename': 'foo.py'}]
        }
    self.verify.Match(p, s, d)

  def testValue(self):
    # TODO: Make it so the newline at the end doesn't screw you up!
    p = """\
        P| Change[ ]{ \d+ change-num }[ ]on[ ]
         | value date
        P|     { \d+ year } / { \d+ month } / { \d+ day }
         | end
        """

    s = """\
Change 35 on 2009/06/20
"""

    d = {
        'change-num': '35',
        'date': {'month': '06', 'day': '20', 'year': '2009'},
        }
    self.verify.Match(p, s, d)

  def testBadRegex(self):
    # Test that bad regular expressions are caught at compile time
    self.verify.CompilationError(_exceptions.PatternSyntaxError, '\\')

  def testScalarValue(self):
    p = """
         | $value digits
        P|    \d+ {.space}
         | end
        """
    self.verify.Match(p, '10 ', {'digits': '10 '})
    self.verify.NoMatch(p, '10!')

  def testScalarValueWithFilter(self):
    p = """
         | $value digits|int
        P|    \d+
         | end
        """
    self.verify.Match(p, '10', {'digits': 10})
    self.verify.Match(p, '10!', {'digits': 10})

  def testLiteralBlock(self):
    p = """  '| ... """
    self.verify.Match(p, '...', {})
    self.verify.NoMatch(p, '!!!')

  def testScalarValueWithLiteralBlock(self):
    p = """\
         | $value digits
        P|     \d+
        '|     ...
         | end
        """
    s = '10...'

    self.verify.Match(p, s, {'digits': '10...'})
    self.verify.NoMatch(p, '10!!!')

  def testLiteralBlockWithScalarValue(self):
    p = "'| \d+{$value foo}..."

    s = "\d+{$value foo}..."
    self.verify.Match(p, s, {})
    self.verify.Match(p, s + 'junk', {})
    self.verify.NoMatch(p, s[:-1])

  def testOptionalScalar(self):
    p = """
         | $optional pending
        P|    \*pending\*
         | end
        """

    self.verify.Match(p, '', {'pending': ''})
    self.verify.Match(p, '*pending*', {'pending': '*pending*'})

    # This matches '', but doesn't consume foo
    self.verify.Match(p, 'foo', {'pending': ''})

  def testOptionalComposite(self):
    p = """
         | optional file
        P|     { \S+ name }[ ]{ \d+ count }
         | end
        """

    self.verify.Match(
        p, 'foo.py 3', {'file': {'name': 'foo.py', 'count': '3'}})

  def testOptionalWithLiteral(self):
    p = """
         | $optional pending
        P|   {.space}
        '|   *pending*
         | end
        """
    self.verify.ParseTree(p)

    self.verify.Match(p, '', {'pending': ''})
    self.verify.Match(p, ' *pending*', {'pending': ' *pending*'})

  def testOptionalWithFilter(self):
    p = """
         | $optional pending|bool
        P|     {.space}
        '|     *pending*
         | end
        """
    self.verify.Match(p, '', {'pending': False})
    self.verify.Match(p, ' *pending*', {'pending': True})

  def testRepeatedWithAnonymousShorthand(self):
    p = """
         | repeated * numbers
        P|     { \d+ }[ ]
         | end
        """
    self.verify.Match(p, '12 23 34 ', {'numbers': ['12', '23', '34']})

    # Now with filter 
    p = """
         | repeated * numbers
        P|     { \d+ | int}[ ]
         | end
        """
    self.verify.Match(p, '12 23 34 ', {'numbers': [12, 23, 34]})

  def testRepeatedWithAnonymousValue(self):
    # NOTE: This has to be *scalar*
    p = """
         | repeated * numbers
         |   $value @
        P|     \d+
         |   end
        P|   [ ]
         | end
        """
    self.verify.Match(p, '12 23 34 ', {'numbers': ['12', '23', '34']})

    # Now with filter 
    p = """
         | repeated * numbers
         |   $value @ | int
        P|     \d+
         |   end
        P|   [ ]
         | end
        """
    self.verify.Match(p, '12 23 34 ', {'numbers': [12, 23, 34]})

  def testRepeatedWithFilter(self):
    # Now apply len
    p = """
         | repeated * numbers | len
         |   $value @
        P|     \d+
         |   end
        P|   [ ]
         | end
        """
    self.verify.Match(p, '12 23 34 ', {'numbers': 3})

  def testAnonymous(self):
    # This pattern has no names at all.  We can capture a scalar at the top
    # level.
    p = """
         | $value @ | int
        P|   \d+
         | end
        """
    self.verify.Match(p, '1234 ', 1234)

    p = 'P| { \d+ | int}'  # synonym
    self.verify.Match(p, '56 ', 56)

  def testReuse(self):
    # Subpattern
    int_pattern = jsonpattern.Pattern('P| { \d+ | int }')

    p = jsonpattern.Pattern('P| {:int seconds}', patterns={'int': int_pattern})
    result = p.match('12s')

    self.verify.Equal(result.data, {'seconds': 12})
    self.verify.Equal(result.pos, 2)

  # TODO: Anonymous version is {:whitespace}

  def testNoopControlsWhitespace(self):
    p = 'SP| {} foo {}'
    self.verify.Match(p, ' foo ', {})
    self.verify.NoMatch(p, ' foo')


class GroupCaptureTest(testy.Test):

  LABELS = ['multilanguage']

  def testErrors(self):
    self.verify.CompilationError(
        _exceptions.PatternSyntaxError,
        """
         |  group[0] desc
        P|    (.+?)\d+
         |  end
        """)

    # TODO: More errors, check that there is exactly one regex in the block

  def testBasic(self):
    p = testy.ClassDef(r"""
         |  group[1] desc
        P|    (.+?)\d+
         |  end
        """)
    self.verify.Match(p, 'foo3', {'desc': 'foo'})
    self.verify.NoMatch(p, 'foo')

    self.verify.Match(p, '333', {'desc': '3'})

  def testCapturingAllGroups(self):
    p = testy.ClassDef(r"""
         |  group[] desc
        P|    (\w+)(\s+)(\w+)
         |  end
        """)
    self.verify.Match(p, 'foo  bar', {'desc': ['foo', '  ', 'bar']})
    self.verify.NoMatch(p, 'foo')


class RegexModifiersTest(testy.Test):

  LABELS = ['multilanguage']

  def testIgnoreCase(self):
    p = testy.ClassDef(r"""
        /ignore-case P|    andy {.space}
                     P|    Andy
        """)

    self.verify.NoMatch(p, 'andy andy')
    self.verify.Match(p, 'andy Andy', {})
    self.verify.NoMatch(p, 'Andy andy')
    self.verify.Match(p, 'Andy Andy', {})

  def testMultiple(self):
    p = testy.ClassDef(r"""
        /verbose/ignore-case P|    [a-z]+ [0-9]+
                             P|    [a-z]+ [0-9]+
        """)

    self.verify.NoMatch(p, 'foo9foo9')
    self.verify.NoMatch(p, 'foo 9foo9')
    self.verify.Match(p, 'foo9foo 9', {})
    self.verify.NoMatch(p, 'foo 9 foo 9')


class FullExampleTest(testy.Test):

  LABELS = ['multilanguage']

  def testSvnLog(self):
    s = test_main.TestData('svn-log.txt')

    i = jsonpattern.Pattern('P| { \d+ | int}')
    d = jsonpattern.Pattern(
        'P| {:int year} - {:int month} - {:int day}',
        patterns={'int': i})
    t = jsonpattern.Pattern(
        'P| {:int hour} : {:int minute} : {:int second}',
        patterns={'int': i})

    p = r"""
   |    repeated changes
  P|        -*\n
 SP|        r{:int rev} \| { \S+ author } \| {:date date} {:time time} \S+ {#}
 SP|        \([^)]+\) \| {:int lines} lines\n

  P|        Changed paths:\n
   |        repeated paths
  P|            \s+ { \w action } \s+ { [^\n]+ path } \n
   |        end
  P|        \n
   |   
  #         TODO: like P4, need a "until next match" phase
  P|        { [^-]* desc }
   |    end
"""
    p = testy.ClassDef(p, patterns={
        'int': i, 'time': t, 'date': d,
        })

    self.verify.Match(p, s, json.loads(test_main.TestData('svn-log.json')))

  def testLs(self):
    s = test_main.TestData('ls.txt')

    i = jsonpattern.Pattern(r'P| { \d+ | int}')
    d = jsonpattern.Pattern(
        r'P| {:int year} - {:int month} - {:int day}',
        patterns={'int': i})
    t = jsonpattern.Pattern(
        r'P| {:int hour} : {:int minute}',
        patterns={'int': i})

    word = jsonpattern.Pattern(r'P| { \w+ }')

    perms = jsonpattern.Pattern(r'P| { [\w\-]+ }')

    # TODO: Get rid of \s+ by implementing I
    p = r"""
   #     Remove leading line:
   P|    .*\n
  SP|    total {:int total}\n

    |    repeated entries
   P|        {:perms perms} \s+ {:int foo} \s+ {:word group} \s+ 
   P|        {:word user} \s+ {:int bytes} \s+ {:Y-M-D date} \s+
   P|        {:H:M time} \s+ { \S+ filename }
    |        optional symlink-target
  SL|            {#} -> { \S+ }
    |        end
   P|        \n
    |    end
"""
    p = testy.ClassDef(p, patterns={
        'int': i,
        'Y-M-D': d,
        'H:M': t,
        'perms': perms,
        'word': word,
        })

    self.verify.Match(p, s, json.loads(test_main.TestData('ls.json')))


if __name__ == '__main__':
  test_main.main(__import__(__name__), sys.argv[1:])
